#ifndef __WIN_SOCKET_CLIENT_H__
  #define __WIN_SOCKET_CLIENT_H__

#include <string>
#include <WinSock.h>

#include "exceptions.h"

#define WIN_SOCKET_CLIENT_BUFFER_SIZE (32 * 1024)

using namespace std;

//! Client for network communications

/*!
This class helps to send requests through network ports to any server. Binary information are not supported by
this implementation. This class can be freely used to send HTTP requests through Internet.

Implementation date: 2008

Usage:

\code
string str_tmp;
win_socket_client wsc;
wsc.open_connection("www.google.com", 80); // direct connection without proxy server on the way
wsc.send_and_receive_data("GET /\r\n\r\n", str_tmp);
wsc.close_connection();
\endcode
*/

class win_socket_client {
private:
  sockaddr_in local;
  SOCKET my_client_socket;
  bool is_connected;

  //! assignment operator: not allowed
  win_socket_client &operator=(const win_socket_client &wsc);

  //! copy constructor: not allowed
  win_socket_client(const win_socket_client &wsc);

  bool is_ip_address(const string &input) const {
    for (size_t i = 0; i < input.length(); ++i) {
      if (isdigit((unsigned char) input[i]) == false && input[i] != '.')
        return false;

    } // for i

    return true;
  }

public:
  //! opens a connection to the given server using its host name or its ip address (may throw an exception)
  void open_connection(const string &host_name_or_ip_address, unsigned short port) {
    close_connection(); // closes the previous connection if any

    my_client_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (my_client_socket == INVALID_SOCKET) {
      WSACleanup();
      throw socket_exception("Cannot initialize the socket");
    } // if

    const bool is_ip = is_ip_address(host_name_or_ip_address);

    local.sin_family = AF_INET; // address family

    if (is_ip == true) {

      unsigned long addr = inet_addr(host_name_or_ip_address.c_str());
      if (addr == INADDR_NONE) {
        string msg = "Invalid host name or IP address '%1'";
        str::format(msg, arg<string>(host_name_or_ip_address));
        throw socket_exception(msg);
      } // if

      local.sin_addr.s_addr = addr;
    } else {
      struct hostent *he = gethostbyname(host_name_or_ip_address.c_str());
      if (he == 0)
        throw socket_exception("Opening the connection failed");

      local.sin_addr = *((struct in_addr*) he->h_addr);
    } // else

    local.sin_port = htons(port); // port to use

    if (connect(my_client_socket, (SOCKADDR*) &local, sizeof(local)) != 0)
      throw socket_exception("Error while connecting");

    is_connected = true;
  }

  //! closes the connection if it is still open (may throw an exception)
  void close_connection() {
    if (is_connected == true) {
      if (closesocket(my_client_socket) != 0)
        throw socket_exception("Closing the connection failed");

      is_connected = false;
    } // if
  }

  //! sends the given string to the server (may throw an exception)
  void send_data(const string &input) {
    if (input.length() > INT_MAX)
      throw socket_exception("Input is too long");

    if (is_connected == false)
      throw socket_exception("Not connected");

    int total_bytes_sent = 0;
    int i = 0;

    do {
      i = send(my_client_socket, input.c_str() + (i * sizeof(char)), (input.length() - total_bytes_sent) * sizeof(char), 0);
      if (i < 0)
        throw socket_exception("Send error");

      total_bytes_sent += i;
    } while (total_bytes_sent < (int) input.length());
  }

  //! sends the given string to the server and receives the response from that server (may throw an exception)
  /*!
  if time_out_in_seconds is equal to ULONG_MAX, then this method will never time out and waits for an answer.
  */
  void send_and_receive_data(const string &input, string &output, unsigned long time_out_in_seconds = 5) {
    fd_set fd_list; // file descriptor list

    output.resize(0);

    FD_ZERO(&fd_list); // initializes the list of file descriptors
    FD_SET(my_client_socket, &fd_list); // adds a file descriptor to the list

    send_data(input);

    int i = 0;
    int sel = -1;
    if (time_out_in_seconds == ULONG_MAX)
      sel = select(my_client_socket + 1, &fd_list, 0, 0, 0); // never times out
    else {
      struct timeval t1;
      t1.tv_sec = time_out_in_seconds; // in seconds
      t1.tv_usec = 0; // in microseconds

      sel = select(my_client_socket + 1, &fd_list, 0, 0, &t1);
    } // else

    if (sel == -1) // blocks until there is something to read / write
      throw socket_exception("Select error");
    else if (sel == 0)
      throw socket_exception("Timed out");

    if (FD_ISSET(my_client_socket, &fd_list) != 0) {
      char temp[WIN_SOCKET_CLIENT_BUFFER_SIZE + 1] = { "\0" };

      do {
        memset(temp, 0, WIN_SOCKET_CLIENT_BUFFER_SIZE + sizeof(char));
        i = recv(my_client_socket, temp, WIN_SOCKET_CLIENT_BUFFER_SIZE * sizeof(char), 0);
        if (i <= 0)
          break; // throw socket_exception("Connection closed unexpectedly");

        output.append(temp, i);
      } while (i <= (int) (WIN_SOCKET_CLIENT_BUFFER_SIZE * sizeof(char)));
    } // if
  }

  //! default constructor (may throw an exception)
  win_socket_client() : is_connected(false) {
    WSADATA wsaData;
    const int wsaret = WSAStartup(0x101, &wsaData); // WSAStartup returns zero on success.
    if (wsaret != 0)
      throw socket_exception("Cannot initialize the socket");
  }

  //! destructor
  virtual ~win_socket_client() {
    try {
      close_connection();
    } catch (const general_exception &ge) {
      WSACleanup();
      throw ge;
    } // catch

    WSACleanup();
  }
};

#endif
